package other.socket;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Server {
    /*
    java.net,ServerSocket
    运行在服务端,作用:
    1向系统申请服务端口,客户端可以通过该端口与服务端建立连接
    2监听服务端口,一旦一个客户端连接了,就会立即建立一个Scoket实例与该客户端交互
     */
    private ServerSocket serverSocket;//总机
    //设计集合存放所有客户端输出流,用于群发
    //private List<PrintWriter> allOut = new ArrayList<>();
    //存放所有客户端的输出流(key:客户端的昵称 value:客户端的输出流)
    private Map<String,PrintWriter> allOut = new HashMap<>();

    public Server(){
        try {
            System.out.println("正在启动服务器");
            /*
            创建ServerSocket对象时,会申请一个端口
            端口被占用,会抛出异常:
            Java.net.BindException:Address already in use
            解决办法:
            1杀死占用该端口的进程(通常发生在服务端启动两次导致)
            2更换端口号
             */
            serverSocket = new ServerSocket(8088);
            System.out.println("服务器启动完毕");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void start(){
        try {
            while (true){
                System.out.println("等待连接...");
            /*
            Socket accept():
            用于接收客户端的连接,并返回一个Scoket实例与连接的客户端交互
            该方法是一个阻塞方法,调用后程序会"卡住",直到一个客户端连接为止
             */
                Socket socket = serverSocket.accept();
                System.out.println("一个客户端连接成功了");

                //启动一个线程来处理该客户端的交互
                ClientHandler handler = new ClientHandler(socket);
                Thread t = new Thread(handler);
                t.start();

            }
        } catch (IOException e) {
        }
    }

    public static void main(String[] args) {
        Server server = new Server();
        server.start();
    }

    //该线程任务用于负责与指定的客户端交互
    private class ClientHandler implements Runnable{
        private Socket socket;
        private String host;//记录客户端的IP地址
        private String nickName;//记录客户端昵称
        public ClientHandler(Socket socket){
            this.socket = socket;
            host = socket.getInetAddress().getHostAddress();//通过socket获取远端(客户端)计算机地址
        }
        public void run(){
            PrintWriter pw = null;
            try {
                //通过socket获取输入流用于读取客户端发送过来的消息
                InputStream in = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
                BufferedReader br = new BufferedReader(isr);

                //通过socket获取输出流用于向客户端发送消息
                OutputStream out = socket.getOutputStream();
                OutputStreamWriter osw = new OutputStreamWriter(out,StandardCharsets.UTF_8);
                BufferedWriter bw = new BufferedWriter(osw);
                pw = new PrintWriter(bw,true);

                nickName = br.readLine();//读写客户端发过来的昵称
                synchronized (allOut){//向集合中添加元素需要添加同步机制,保证多个线程不能同时添加
                    allOut.put(nickName,pw);//将客户端的昵称和输出流存入Map中

                }
                sendMessageToAll("上线了,当前在线人数:"+allOut.size());//通知所有客户端,该用户上线了

                /*
                    当我们通过br.readLine()读取对方发送来的一行字符串时,
                    远端计算机如果调用scoket.close()正常与我们断开连接(进行四次挥手),
                    那么readLine()方法会返回null
                    若对方是异常中端(强制杀死进程),此时readLine()会抛出异常.
                 */

                String message;
                while((message=br.readLine())!=null){//读取客户端发送过来的消息
                    if(message.startsWith("@")){//如果聊天内容以"@"开头则表示为私聊
                        sendMessageToSomeone(message);//调用私聊方法实现私聊
                    }else{//否则为群聊
                        sendMessageToAll("说:"+message);//调用群聊方法实现群聊
                    }
                }
            } catch (IOException e) {
            }finally{
                //处理客户端下线后的操作:共三步:
                synchronized (allOut){
                    allOut.remove(nickName);//1)根据昵称将该客户端的输出流从共享集合中删除

                }
                sendMessageToAll("下线了,当前在线人数:"+allOut.size());//2)通过所有客户端,该用户下线了
                try {
                    socket.close();//3)关闭socket
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        //将消息发送给所有客户端(群聊)
        private void sendMessageToAll(String message){
            System.out.println(nickName+"["+host+"]"+message);
            synchronized (allOut){//群发时也要保证多个线程同步处理
                /*
                    新循环遍历集合就是迭代器,因为迭代器要求遍历过程中不可以用集合的方式来增删元素,
                    因此为了保证遍历正确,防止其它线程增删,这里要和其他线程的增删互斥
                 */
                for(PrintWriter o : allOut.values()){
                    o.println(nickName+"["+host+"]"+message);
                }
            }
        }
        //将消息发送给指定用户(私聊)
        private void sendMessageToSomeone(String message){
            //message的样子:@范传奇:你好!
            if(message.matches("@.+:.+")){//验证私聊格式是否正确
                String toNickName = message.substring(1,message.indexOf(":"));//提取对方的昵称
                if(allOut.containsKey(toNickName)){//在线
                    PrintWriter pw = allOut.get(toNickName);//获取对方的输出流
                    String toMessage = message.substring(message.indexOf(":")+1);//截取私聊内容
                    pw.println(nickName+"悄悄对你说:"+toMessage);

                }else{//不在线
                    PrintWriter pw = allOut.get(nickName);//根据昵称获取当前客户端的输出流
                    pw.println("该用户["+toNickName+"]不在线!");
                }
            }else{//格式不对
                PrintWriter pw = allOut.get(nickName);//根据昵称获取其对应的输出流
                pw.println("私聊格式不正确!");
            }
        }
    }
}
